/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.mapping;

import java.util.*;
import javax.swing.*;
import javax.swing.table.*;
import org.jdesktop.beansbinding.*;
import org.jdesktop.beansbinding.AutoBinding.*;
import org.jdesktop.swingbinding.*;
import ch.trackedbean.binding.beanSpec.*;
import ch.trackedbean.binding.guicomponents.*;
import ch.trackedbean.binding.guicomponents.renderers.*;
import ch.trackedbean.loaders.lists.*;

/**
 * Mapping for {@link JTable}s.<br>
 * Consider using the {@link TablePanel}.
 * 
 * @author M. Hautle
 * @param <T> The bean type
 */
public class TableMapping<T> {
	/** The name of the model property. */
	public static final String MODEL_PROPERTY = "model";

	// CHECKME property validation?

	/** Property for table selection */
	protected static final Property TABLE_SELECTION_PROP = BeanProperty.<JTable, Object> create("selectedElement_IGNORE_ADJUSTING");

	/** Property for {@link MappingGroup} */
	protected static final Property MAPPING_GRP_PROP = BeanProperty.<MappingGroup, Object> create("model");

	/** The mapped table */
	protected final JTable table;

	/** The binding of the table */
	protected final JTableBinding<T, List<T>, JTable> binding;

	/** The selection mapping or null */
	protected HashMap<String, Binding> selectionBindings = null;

	/** Map containing informations for the column bindings */
	protected final IdentityHashMap<JTableBinding<T, List<T>, JTable>.ColumnBinding, ColumnBindingInfos> bindingInfos = new IdentityHashMap<JTableBinding<T, List<T>, JTable>.ColumnBinding, ColumnBindingInfos>();

	/** Map holding the list bindings for a given {@link BindingInfos} object. */
	protected final IdentityHashMap<ListBindingInfos, JComboBoxBinding> listBindings = new IdentityHashMap<ListBindingInfos, JComboBoxBinding>();

	/** The list used as table model */
	protected List<T> model;

	/** Flag indicating if the table is read only. */
	protected boolean readOnly;

	/**
	 * Default constructor
	 * 
	 * @param table The table to map
	 * @param readOnly True if the table is readonly
	 */
	public TableMapping(final JTable table, boolean readOnly) {
		this.table = table;
		this.readOnly = readOnly;
		binding = SwingBindings.createJTableBinding(AutoBinding.UpdateStrategy.READ, model, table);
	}

	/**
	 * Adds a selection binding to the given {@link MappingGroup}.<br>
	 * If a binding with the same name exists already, then it's removed before the new one is added.
	 * 
	 * @param name The name of the selection binding
	 * @param group The binding group to add
	 */
	@SuppressWarnings("unchecked")
	public void addSelectionBinding(final String name, final MappingGroup<T> group) {
		final Binding b = Bindings.createAutoBinding(AutoBinding.UpdateStrategy.READ, table, TABLE_SELECTION_PROP, group, MAPPING_GRP_PROP);
		addSelectionBinding(name, b);
	}

	/**
	 * Adds a selection binding to the given {@link MappingGroup}.<br>
	 * If a binding with the same name exists already, then it's removed before the new one is added.
	 * 
	 * @param name The name of the selection binding
	 * @param relativePath The path starting at the returned row
	 * @param group The binding group to add
	 */
	@SuppressWarnings("unchecked")
	public void addSelectionBinding(final String name, final String relativePath, final MappingGroup group) {
		final Binding b = Bindings.createAutoBinding(AutoBinding.UpdateStrategy.READ, table, BeanProperty.create(TABLE_SELECTION_PROP, relativePath), group,
				MAPPING_GRP_PROP);
		addSelectionBinding(name, b);
	}

	/**
	 * Adds a selection binding to the given bean property.<br>
	 * If a binding with the same name exists already, then it's removed before the new one is added.
	 * 
	 * @param name The name of the selection binding
	 * @param dst The destination bean
	 * @param dstPath The path to the destination property
	 */
	@SuppressWarnings("unchecked")
	public void addSelectionBinding(final String name, final Object dst, final String dstPath) {
		final Binding b = Bindings.createAutoBinding(AutoBinding.UpdateStrategy.READ, table, TABLE_SELECTION_PROP, dst, BeanProperty.create(dstPath));
		addSelectionBinding(name, b);
	}

	/**
	 * Adds a selection binding to the given bean property.<br>
	 * If a binding with the same name exists already, then it's removed before the new one is added.
	 * 
	 * @param name The name of the selection binding
	 * @param relativePath The path starting at the returned row
	 * @param dst The destination bean
	 * @param dstPath The path to the destination property
	 */
	@SuppressWarnings("unchecked")
	public void addSelectionBinding(final String name, final String relativePath, final Object dst, final String dstPath) {
		final Binding b = Bindings.createAutoBinding(AutoBinding.UpdateStrategy.READ, table, BeanProperty.create(TABLE_SELECTION_PROP, relativePath), dst,
				BeanProperty.create(dstPath));
		addSelectionBinding(name, b);
	}

	/**
	 * Adds the given binding as selection binding.<br>
	 * If a binding with the same name exists already, then it's removed before the new one is added.
	 * 
	 * @param name The name of the selection binding
	 * @param b The binding
	 */
	protected void addSelectionBinding(final String name, final Binding b) {
		if (selectionBindings == null)
			selectionBindings = new HashMap<String, Binding>();
		final Binding old = selectionBindings.put(name, b);
		if (old != null)
			old.unbind();
		b.bind();
	}

	/**
	 * Removes the selection binding with the given name
	 * 
	 * @param name The name of the selection binding
	 */
	public void removeSelectionBinding(final String name) {
		if (selectionBindings == null)
			return;
		final Binding b = selectionBindings.remove(name);
		if (b != null)
			b.unbind();
	}

	/**
	 * Adds a column binding
	 * 
	 * @param label The column label
	 * @param path The path to the property shown in this column
	 * @param editable True if the column is editable
	 * @return The {@link BindingInfos} object for further configuration of the binding
	 * @throws IllegalStateException if the table is already bound
	 */
	public ColumnBindingInfos addColumn(final String label, final String path, final boolean editable) {
		if (model != null)
			throw new IllegalStateException("Can't add a column if the table is already bound");
		final JTableBinding<T, List<T>, JTable>.ColumnBinding b = binding.addColumnBinding(BeanProperty.<T, Object> create(path));
		b.setColumnName(label);
		final ColumnBindingInfos i = new ColumnBindingInfos(path);
		i.setEditable(editable);
		bindingInfos.put(b, i);
		return i;
	}

	/**
	 * Adds a column binding
	 * 
	 * @param label The column label
	 * @param path The path to the property shown in this column
	 * @param type The type of this column
	 * @param editable True if the column is editable
	 * @return The {@link BindingInfos} object for further configuration of the binding
	 * @throws IllegalStateException if the table is already bound
	 */
	public ColumnBindingInfos addColumn(final String label, final String path, final Class<?> type, final boolean editable) {
		if (model != null)
			throw new IllegalStateException("Can't add a column if the table is already bound");
		final JTableBinding<T, List<T>, JTable>.ColumnBinding b = binding.addColumnBinding(BeanProperty.<T, Object> create(path));
		b.setColumnName(label);
		b.setColumnClass(type);
		final ColumnBindingInfos i = new ColumnBindingInfos(path);
		i.setEditable(editable);
		bindingInfos.put(b, i);
		return i;
	}

	/**
	 * Adds a column binding with a {@link JComboBox} using a {@link SimpleELListRenderer} with the given el expression.<br>
	 * The combobox content list will be loaded using {@link ListManager#getList(Class, String, Map)}.
	 * 
	 * @param label The column label
	 * @param path The path to the property shown in this column
	 * @param el The simple el expression to use by the combobox renderer
	 * @param type The content type of the combobox list
	 * @param subType Additional string specifying the "subtype" of the combobox content list (may be null)
	 * @param settings Additional settings for the combobox content load (may be null)
	 * @return The {@link BindingInfos} object for further configuration of the binding
	 * @throws IllegalStateException if the table is already bound
	 */
	public ColumnBindingInfos addComboBoxColumn(final String label, final String path, String el, Class<?> type, String subType, Map<Object, Object> settings) {
		final ColumnBindingInfos i = addColumn(label, path, true);
		final JComboBox combobox = new JComboBox();
		combobox.setRenderer(new SimpleELListRenderer(el, ""));
		final JComboBoxBinding<Object, List<Object>, JComboBox> listBinding = SwingBindings.createJComboBoxBinding(UpdateStrategy.READ, null, combobox);
		i.setRenderer(new SimpleELTableCellRenderer(el, "", this, false));
		i.setEditor(new DefaultCellEditor(combobox));
		i.setSubBinding(new ListBindingInfos(listBinding, type, subType, settings));
		return i;
	}

	/**
	 * Adds a column binding to a statusproperty
	 * 
	 * @param label The column label
	 * @param path The path to the property
	 * @throws IllegalStateException if the table is already bound
	 */
	public void addStatusColumn(final String label, final String path) {
		if (model != null)
			throw new IllegalStateException("Can't add a column if the table is already bound");
		final JTableBinding<T, List<T>, JTable>.ColumnBinding b = binding.addColumnBinding(StatusProperty.<T> create(path));
		b.setColumnName(label);
		b.setColumnClass(Status.class);
		b.setEditable(false);
	}

	/**
	 * Removes the binding for the column with the given index
	 * 
	 * @param col The column to remove
	 * @return True if the column was removed
	 * @throws IllegalStateException if the table is already bound
	 */
	public boolean removeColumnBinding(final int col) {
		if (model != null)
			throw new IllegalStateException("Can't remove a column if the table is already bound");
		if (col >= table.getColumnCount())
			return false;
		final JTableBinding<T, List<T>, JTable>.ColumnBinding bn = binding.removeColumnBinding(col);
		bindingInfos.remove(bn);
		return true;
	}

	/**
	 * Returns the status of the value in the given cell
	 * 
	 * @param row The row
	 * @param col The column
	 * @return The status of the value in the given cell or null if the cell doesen't exists
	 */
	public Status getCellStatus(final int row, final int col) {
		if (row >= model.size() || col > table.getColumnCount())
			return null;
		final BindingInfos info = bindingInfos.get(binding.getColumnBinding(table.convertColumnIndexToModel(col)));
		if (info == null)
			return null;
		return BeanStatusImpl.getPropertyStatus(model.get(row), info.getProperty());
	}

	/**
	 * Returns if the table is readonly.
	 * 
	 * @return If the table is readOnly
	 */
	public boolean isReadOnly() {
		return readOnly;
	}

	/**
	 * Sets if the table is read only
	 * 
	 * @param readOnly True if the table has to be readonly
	 */
	public void setReadOnly(final boolean readOnly) {
		if (model != null)
			throw new IllegalStateException("Can't set the table to readonly if it's already bound");
		this.readOnly = readOnly;
	}

	/**
	 * Sets the given model.<br>
	 * If null is passed {@link #removeModel()} is called.<br>
	 * If the table is already bound to a model then the table will be rebound to the new model.
	 * 
	 * @param model The model to set if null is passed {@link #removeModel()} is called
	 */
	public void setModel(final List<T> model) {
		if (model == null) {
			removeModel();
			return;
		}
		if (this.model != null)
			removeModel();
		bind(model);
	}

	/**
	 * Returns the mapped table.
	 * 
	 * @return The table
	 */
	public JTable getTable() {
		return table;
	}

	/**
	 * Returns the list model
	 * 
	 * @return The list model or null
	 */
	public List<T> getModel() {
		return model;
	}

	/**
	 * Returns the object representing the selected row.
	 * 
	 * @return The selected object or null
	 */
	public T getSelectedRow() {
		final int row = table.getSelectedRow();
		if (row == -1)
			return null;
		return model.get(table.convertRowIndexToModel(row));
	}

	/**
	 * Returns a list of all selected rows.
	 * 
	 * @return The selected rows
	 */
	public List<T> getSelectedRows() {
		final int cnt = table.getSelectedRowCount();
		if (cnt == 0)
			return Collections.emptyList();
		// fill the selection list
		final List<T> res = new ArrayList<T>(cnt);
		for (int i : table.getSelectedRows())
			res.add(model.get(table.convertRowIndexToModel(i)));
		return res;
	}

	/**
	 * Binds the table to the given model
	 * 
	 * @param list The model
	 */
	protected void bind(final List<T> list) {
		this.model = list;
		// configure the bindings
		for (final JTableBinding<T, List<T>, JTable>.ColumnBinding b : binding.getColumnBindings()) {
			final ColumnBindingInfos info = bindingInfos.get(b);
			if (info == null)
				continue;
			info.configure(b);
			info.bindSubBinding();
		}
		binding.setSourceObject(list);
		binding.setEditable(!readOnly);
		binding.bind();
		configureTable();
	}

	/**
	 * Configures the table.<br>
	 * This method has to be called after the model was bound!
	 */
	protected void configureTable() {
		final TableColumnModel cols = table.getColumnModel();
		for (final JTableBinding<T, List<T>, JTable>.ColumnBinding b : binding.getColumnBindings()) {
			final ColumnBindingInfos info = bindingInfos.get(b);
			if (info != null)
				configureColumn(cols.getColumn(b.getColumn()), info);
		}
	}

	/**
	 * Configures the given column according to the binding info.
	 * 
	 * @param col The column
	 * @param infos The binding info
	 */
	protected void configureColumn(TableColumn col, ColumnBindingInfos infos) {
		col.setCellRenderer(infos.getRenderer());
		col.setCellEditor(infos.getEditor());
	}

	/**
	 * Removes the current model
	 */
	public void removeModel() {
		if (binding.isBound())
			binding.unbind();
		this.model = null;
	}
}
